﻿using System;
using System.IO;
using Tesla.Content;
using Tesla.Content.Loaders.Image;
using Tesla.Content.Loaders.Model;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Input;
using Tesla.Input.Control;
using Tesla.Math;
using Tesla.Scene;
using Tesla.Util;

namespace TeslaSamples {
    /// <summary>
    /// Simple application class that runs our samples.
    /// </summary>
    public abstract class BasicApp : IApp {
        private Node _rootNode;
        private Window _window;
        private IWindowHost _host;
        private GameTimer _timer;
        private IRenderer _renderer;
        private Color _clearColor;
        private InputLayer _inputLayer;
        private ContentManager _content;

        private RenderSystemPlatform _platform;
        private bool _exiting;
        private bool _debugNormals;
        private bool _debugBounds;
        private bool _wireFrame;
        private bool _wireFrameSolid;
        private bool _instructions;
        private Material _wireFrameSolidMat;
        private Texture2D _instructionTex;
        private SpriteBatch _spriteBatch;
        private SpriteFont _instructionFont;

        //Variables for measuring the frame rate
        private int _frameRate;
        private float _frameAccumulation;
        private int _frameCounter;

        /// <summary>
        /// Gets or sets the root node, e.g. the scene graph.
        /// </summary>
        public Node RootNode {
            get {
                return _rootNode;
            }
            set {
                _rootNode = value;
            }
        }

        /// <summary>
        /// Gets the window we're rendering to.
        /// </summary>
        public Window Window {
            get {
                return _window;
            }
        }

        /// <summary>
        /// Gets the timer used during Update().
        /// </summary>
        public GameTimer Timer {
            get {
                return _timer;
            }
        }

        /// <summary>
        /// Gets the renderer we use during Render().
        /// </summary>
        public IRenderer Renderer {
            get {
                return _renderer;
            }
        }

        /// <summary>
        /// Gets or sets the color the backbuffer should be cleared to.
        /// </summary>
        public Color ClearColor {
            get {
                return _clearColor;
            }
            set {
                _clearColor = value;
            }
        }

        /// <summary>
        /// Gets the input layer.
        /// </summary>
        public InputLayer InputLayer {
            get {
                return _inputLayer;
            }
        }

        /// <summary>
        /// Gets the content manager.
        /// </summary>
        public ContentManager ContentManager {
            get {
                return _content;
            }
        }

       private void Init(RenderSystemPlatform platform, PresentationParameters pp) {
           //Check the platform versioning and create the proper defaults
           OperatingSystem os = System.Environment.OSVersion;
           IRenderSystemProvider renderSystem;
           switch(platform) {
               case RenderSystemPlatform.Direct3D10:
                   if(os.Platform != PlatformID.Win32NT) {
                       throw new TeslaException("Renderer not available for OS that is running.");
                   }
                   renderSystem = new Tesla.Direct3D10.Graphics.D3D10RenderSystemProvider();
                   Engine.Initialize(renderSystem);
                   Engine.Services.AddService(typeof(IWindowProvider), new Tesla.Windows.Core.SWFWindowProvider());
                   Engine.Services.AddService(typeof(IKeyboardProvider), new Tesla.Windows.Input.WindowsKeyboardProvider());
                   Engine.Services.AddService(typeof(IMouseProvider), new Tesla.Windows.Input.WindowsMouseProvider());
                   break;
                   /*
               case RenderSystemPlatform.XNA4:
                   if(os.Platform != PlatformID.Win32NT) {
                        throw new TeslaException("Renderer not available for OS that is running.");
                   }
                   renderSystem = new Tesla.Xna.Graphics.XNARenderSystemProvider();
                   Engine.Initialize(renderSystem);
                   Engine.Services.AddService(typeof(IWindowProvider), new Tesla.Windows.Core.SWFWindowProvider());
                   Engine.Services.AddService(typeof(IKeyboardProvider), new Tesla.Windows.Input.WindowsKeyboardProvider());
                   Engine.Services.AddService(typeof(IMouseProvider), new Tesla.Windows.Input.WindowsMouseProvider());
                   break;*/
               default:
                   throw new TeslaException("Platform not supported");
           }

           //Set renderer/platform
           _renderer = renderSystem.Renderer;
           _platform = platform;
           _clearColor = Color.CornflowerBlue;

           //Create the window and the host we'll be running it from
           _window = new Window(pp);
           _window.ClientSizeChanged += new EventHandler(ResizeViewport);
           _host = Engine.Services.GetService<IWindowProvider>().CreateWindowHost(_window);

           //Create the camera
           Camera camera = new Camera(new Viewport(0, 0, _window.ClientBounds.Width, _window.ClientBounds.Height));
           camera.Position = new Vector3(0, 0, 200f);
           camera.LookAt(Vector3.Zero, Vector3.Up);
           camera.SetProjection(45f, 1.0f, 500.0f);
           camera.Update();
           _renderer.CurrentCamera = camera;

           //Setup input
           _inputLayer = new InputLayer();
           Mouse.Handle = _window.Handle;
           Mouse.SetPosition(pp.BackBufferWidth / 2, pp.BackBufferHeight / 2);
           FirstPersonControl control = new FirstPersonControl(camera, _inputLayer);

           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.Escape, false),
               new InputAction(delegate(GameTime time) {
               Exit();
           })));
           
           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.Space, false),
               new InputAction(delegate(GameTime time) {
                   _window.IsFullScreen = !_window.IsFullScreen;
           })));

           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.N, false),
               new InputAction(delegate(GameTime time) {
                   ToggleDebugNormals();
           })));

           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.B, false),
               new InputAction(delegate(GameTime time) {
                   ToggleDebugBounding();
           })));

           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.M, false),
                new InputAction(delegate(GameTime time) {
                    ToggleWireFrame();
           })));

           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.V, false),
                new InputAction(delegate(GameTime time) {
                    ToggleWireFrameSolid();
           })));
           _inputLayer.RegisterTrigger(new InputTrigger(new KeyPressedCondition(Keys.F4, false),
               new InputAction(delegate(GameTime time) {
                   ToggleInstructions();
           })));

           //Setup content manager
           switch(platform) {
               case RenderSystemPlatform.Direct3D10:
                   _content = new ContentManager(new MultiFileResourceLocator(Path.Combine(ResourceUtils.TitlePath, "Content"), Path.Combine(ResourceUtils.TitlePath, "Content//D3D10")));
                   _content.RegisterLoader(new Tesla.Direct3D10.Content.Loaders.D3D10EffectLoader());
                   break;
               case RenderSystemPlatform.XNA4:
                   _content = new ContentManager(new MultiFileResourceLocator(Path.Combine(ResourceUtils.TitlePath, "Content"), Path.Combine(ResourceUtils.TitlePath, "Content//XNA4")));
                 //  _content.RegisterLoader(new Tesla.Xna.Content.Loaders.XNAEffectLoader());
                   break;
           }
           _content.RegisterLoader(new DDSLoader());
           _content.RegisterLoader(new PNGLoader());
           _content.RegisterLoader(new JPGLoader());
           _content.RegisterLoader(new BMPLoader());
           _content.RegisterLoader(new DXSLoader());
           _content.RegisterLoader(new BMFontLoader());

           //Setup resources for instruction GUI
           _instructionTex = new Texture2D(1, 1);
           _instructionTex.SetData<Color>(new Color[] { Color.Black });

           _spriteBatch = new SpriteBatch(renderSystem);
           _instructionFont = _content.Load<SpriteFont>("Fonts//comicsans.fnt");
           _wireFrameSolidMat = ContentManager.Load<Material>("BasicColor.tem").Clone();
           _wireFrameSolidMat.SetRenderState(RasterizerState.CullNoneWireframe);
           _wireFrameSolidMat.SetParameter("DiffuseColor", Color.DarkRed);

           //Create the scene graph
           _rootNode = new Node("Scene");

           //Simple point light
           PointLight pl = new PointLight();
           pl.Attenuate = false;
           pl.Position = new Vector3(-100, 75, 100);
           pl.Ambient = Color.Black;
           _rootNode.AddLight(pl);
           LoadContent();
        }

       int count = 0;

       private int Increment() {
           return count++;
       }

       protected void ResizeViewport(Object sender, EventArgs args) {
           Rectangle rect = Window.ClientBounds;
           ICamera cam = Renderer.CurrentCamera;
           cam.Viewport = new Viewport(0, 0, rect.Width, rect.Height);
           cam.SetProjection(45, 0.1f, 30000);
           cam.Update();
           Renderer.CurrentCamera = cam;
       }

        /// <summary>
        /// Runs the application.
        /// </summary>
        /// <param name="platform">Render system platform</param>
        /// <param name="pp">Parameters for backbuffer/window creation</param>
       public void Run(RenderSystemPlatform platform, PresentationParameters pp) {
           Init(platform, pp);

           //Create the timer for interpolation during Update() calls
           _timer = new GameTimer();
           //Set the timer to the engine's data value map, so we can access Time,TimePerFrame,and PerFrame engine values for our shaders
           Engine.ValueMap.Timer = _timer;
           _timer.Start();
           //Create and run the host
           _host.Idle += new Heartbeat(Loop);
           _host.Run();
       }
        //The "Game Loop"
       private void Loop() {
           //Update and render, unless if we're in the middle of exiting
           if(!_exiting) {
               GameTime time = _timer.Update();
               //Accumulate the update to measure FPS
               UpdateFrameRate(time);

               if(!_window.IsMinimized) {
                   _inputLayer.CheckTriggers(time);
                   _renderer.CurrentCamera.Update();
               }

               Update(time);
               Render(_renderer);
           } else {
               //Clean up and exit
               UnloadContent();
               _host.Exit();
               Engine.CleanUpResources();
           }
       }

        /// <summary>
        /// Initiates an exit from the application.
        /// </summary>
       public void Exit() {
           _exiting = true;
       }

        /// <summary>
        /// Use to load any content, this is called once during Run()
        /// </summary>
        protected abstract void LoadContent();

        /// <summary>
        /// Use to unload any extra content. Ensure the base method is called as it call's the Unload() method in the content manager.
        /// </summary>
        protected virtual void UnloadContent() {
            _content.Unload();
        }

        protected virtual void Update(GameTime time) {
            _rootNode.Update(time);

            OnUpdate(time);
        }

        /// <summary>
        /// Called after the scene graph is updated during Update().
        /// </summary>
        /// <param name="time">Time interpolation</param>
        protected virtual void OnUpdate(GameTime time) {}

        protected virtual void Render(IRenderer renderer) {
            //Get the window's swap chain
            SwapChain swapChain = _window.SwapChain;

            //Primary drawing to the window, sets the backbuffer render targets and clears them
            swapChain.Clear(_clearColor);

            if(_wireFrame) {
                renderer.EnforcedRasterizerState = RasterizerState.CullNoneWireframe;
            }

            OnRenderPre(renderer);

            //Draw the scenegraph
            _rootNode.Draw(renderer);

            //Debbuging for bounding volumes
            if(_debugBounds) {
                GeometryDebugger.DrawBounds(_rootNode, renderer);
            }

            //Debugging for normals and tangents/binormals if they exist for the mesh
            if(_debugNormals) {
                GeometryDebugger.DrawTangentBasis(_rootNode, renderer);
            }

            if(_wireFrameSolid) {
                renderer.RenderQueue.SortAndRender();
                //renderer.EnforcedRasterizerState = RasterizerState.CullNoneWireframe;
                RasterizerState rs = new RasterizerState();
                rs.Cull = CullMode.None;
                rs.DepthBias = -100;
                rs.SlopeScaledDepthBias = 5f;
                rs.Fill = FillMode.WireFrame;
                renderer.EnforcedRasterizerState = rs;
                renderer.EnforcedMaterial = _wireFrameSolidMat;
                renderer.RenderQueue.RenderBuckets();
                renderer.RenderQueue.ClearBuckets();

                renderer.ClearEnforcedStates();
                renderer.EnforcedMaterial = null;

            } else {
                //Finally sort the render queue and draw remaining elements
                renderer.RenderQueue.SortRenderClear();
            }

            //Any extra rendering
            OnRenderPost(renderer);

            if(_wireFrame || _wireFrameSolid) {
                renderer.ClearEnforcedState(RenderStateType.RasterizerState);
            }

            //Lastly, draw GUI - we do this via a spritebatch and do it last
            //so it's rendered over everything (including anything from OnRender()
            DrawInstructions(renderer);

            //Present to the window
            swapChain.Present();

            //Count the frame for FPS display
            CountFrame();
        }

        /// <summary>
        /// Called after the render queue is sorted, rendered, and cleared.
        /// </summary>
        /// <param name="renderer">Renderer</param>
        protected virtual void OnRenderPost(IRenderer renderer) {}

        /// <summary>
        /// Called before the renderqueue is sorted, rendered, and cleared and before the scnee graph is drawn.
        /// </summary>
        /// <param name="renderer">Renderer</param>
        protected virtual void OnRenderPre(IRenderer renderer) { }

        /// <summary>
        /// Toggle debugger to draw normals of visible geometry.
        /// </summary>
        protected void ToggleDebugNormals() {
            _debugNormals = !_debugNormals;
        }

        /// <summary>
        /// Toggle debugger to draw bounds of visible geometry.
        /// </summary>
        protected void ToggleDebugBounding() {
            _debugBounds = !_debugBounds;
        }

        /// <summary>
        /// Toggles wireframe.
        /// </summary>
        protected void ToggleWireFrame() {
            _wireFrame = !_wireFrame;
        }

        /// <summary>
        /// Toggles wireframe drawn over solid objects.
        /// </summary>
        protected void ToggleWireFrameSolid() {
            _wireFrameSolid = !_wireFrameSolid;
        }

        /// <summary>
        /// Toggle instruction screen.
        /// </summary>
        protected void ToggleInstructions() {
            _instructions = !_instructions;
        }

        /// <summary>
        /// Draws a semi-transparent full-screen quad and then a list of keyboard controls.
        /// </summary>
        /// <param name="renderer"></param>
        private void DrawInstructions(IRenderer renderer) {
            Viewport vp = renderer.Viewport;
            _spriteBatch.Begin();
            _spriteBatch.DrawString(_instructionFont, String.Format("FPS: {0}", _frameRate), new Vector2(0, 0), Color.White);
            String str = "F4 - Instructions";
            Vector2 dim = _instructionFont.MeasureString(str);
            _spriteBatch.DrawString(_instructionFont, str, new Vector2(vp.Width - dim.X, 0), Color.White);
            if(_instructions) {
                _spriteBatch.Draw(_instructionTex, new Rectangle(0, 0, vp.Width, vp.Height), new Color(0,0,0,.5f));
                _spriteBatch.DrawString(_instructionFont, "Controls:\n\nSpace - FullScreen\nEsc - Exit\nN - Debug Normals\nB - Debug Boundings\nM - Toggle Wireframe\nV - Toggle Wireframe + Solid\nWSAD - Move camera (FPS-Like)",
                    new Vector2(vp.Width * .1f, vp.Height * .1f), Color.White);
            }
            _spriteBatch.End();
        }

        /// <summary>
        /// Update the frame accumulation, this should be done when we're calling Update().
        /// Once we accumulate one second worth of elapsed time, we reset the frame counter.
        /// The frame counter tracks how many times we call Render().
        /// </summary>
        /// <param name="time"></param>
        private void UpdateFrameRate(GameTime time) {
            _frameAccumulation += (float) time.ElapsedTimeInSeconds;
            if(_frameAccumulation >= 1.0f) {
                _frameRate = _frameCounter;
                _frameCounter = 0;
                _frameAccumulation -= 1.0f;
            }
        }

        /// <summary>
        /// Simple frame ticker, call everytime we Render(). 
        /// </summary>
        private void CountFrame() {
            _frameCounter++;
        }
    }
}
